package br.unisul.redeNeural;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;



/**
 * Classe abstrata que representa uma rede neural<br>
 * Nela sera cadastrada suas funções basicas tais com {@link AtualizarPeso}, {@link AtualizarVies}, {@link CalculoSaida},
 * {@link TesteParada}, {@link DefaultVerificarSaida}, {@link FuncaoAtivacao} e {@link InicialiazarValor}<br>
 * Nesta classe tambem cria os {@link Neuronio}s e os {@link Vies} atraves de uma {@link Configuracao}<br>
 * O processo para o treino se resume em inicializar a {@link RedeNeural}, chamar o método <code>inicializarValores()</code> e 
 * implementar o método abstrato <code>treinar(ParTreino);</code>
 * @author Joelson
 * @version 1.0
 * @created 06-Mar-2009
 */
public abstract class RedeNeural implements java.io.Serializable{


	/**
	 * 
	 */
	private static final long serialVersionUID = 7692571604791490937L;

	/**
	 * Mapa de Pesos: camada - pesos[][]
	 */
	private Map<Integer, Peso[][]> pesos;

	/**
	 * Mapa de Neuronios: camada - Neuronios[]
	 */
	private Map<Integer, Neuronio[]> neuronios;
	
	/**
	 * configurações
	 */
	private Configuracao configuracao;

	/**
	 * Atualizar Peso
	 */
	private Atualizar<Peso> atualizarPeso;
	/**
	 * Atualizar vies
	 */
	private Atualizar<Vies> atualizarVies;
	/**
	 * Calcular a saida
	 */
	private CalculoSaida calculoSaida;
	/**
	 * Fazer teste de parada
	 */
	private TesteParada testePara;
	/**
	 * verificar as saidas
	 */
	private VerificarSaida verificarSaida;
	/**
	 * Calcular a função de ativação
	 */
	private FuncaoAtivacao funcaoAtivacao;
	/**
	 * Inicializar os valoress
	 */
	private InicialiazarValor inicialiazarValor;



	/**
	 * Cria uma {@link RedeNeural} com configuraçõess default.<br>
	 * A configuração default implementa os seguintes metodos:
	 * <ul>
	 * <li>Verificação de saida: {@link DefaultVerificarSaida};
	 * <li>Atualização de Peso {@link AtualizarPeso};
	 * <li>Atualização de Vies: {@link AtualizarVies};
	 * <li>Calculo de saida: {@link DefaultCalculoSaida};
	 * <li>Teste de Para: {@link Iteracoes};
	 * <li>Função de Ativação: {@link DefaultFuncaoAtivacao};
	 * <li>Inicialização de Valores {@link DefaultInicializacao}
	 * </ul>
	 * @param configuracao - {@link Configuracao} da rede neural
	 */
	public RedeNeural(Configuracao configuracao) {
		super();
		this.configuracao = configuracao;
		pesos = new HashMap<Integer, Peso[][]>();
		neuronios = new HashMap<Integer, Neuronio[]>();
		verificarSaida = new DefaultVerificarSaida(this);
		atualizarPeso = new AtualizarPeso();
		atualizarVies = new AtualizarVies();
		calculoSaida = new DefaultCalculoSaida();
		testePara = new Iteracoes(10);
		funcaoAtivacao = new DefaultFuncaoAtivacao();
		inicialiazarValor = new DefaultInicializacao(DefaultInicializacao.RADOM);
	}


	/**
	 * Inicializa os {@link Peso} e os {@link Neuronio} conforme a {@link Configuracao}.<br>
	 * A chamada deste método é necessario antes de ser treinada a {@link RedeNeural}.<br>
	 * 
	 */
	public void inicializarValores(){
		criarNeuronios();
		criarPesos();
	}

	/**
	 * Método para treinar um {@link RedeNeural}, neste método deverá chamar o método <b>inicializarValores()</b>
	 * desta mesma classe
	 * @param valoreTreino - valores que serao utilizados durante o treino
	 */
	public abstract void treinar(ParTreino valoresTreino) throws RedeNeuralException;

	/**
	 * Atualiza os valores da camada de entrada, para que possa ser usuada no treino
	 * @param entradas - novas entradas da rede neural
	 * @throws RedeNeuralException caso o tamanho da entrada e da saida seja incompativel com a da configura��o
	 */
	public void atualizarEntradas(double[] entradas)throws RedeNeuralException{
		if(!(entradas.length == configuracao.getQuantidadeEntrada())){
			throw new RedeNeuralException("Tamanho das entradas ou das saidas estão incompativeis com a configuração");
		}
		NeuronioEntrada[] getEntradas = getEntradas();
		for (int i = 0; i < getEntradas.length; i++) {
			getEntradas[i].setValorNeuronio(entradas[i]);
		}

	}


	/**
	 * Calcula a quantidade de camada que a rede neural possui
	 * @return quantidade de camadas que a rede neural possui
	 */
	public int quantidadeCamadas(){
		return neuronios.size();
	}


	/**
	 * Retorna o neuronio que se encontra em uma determinada camada e uma determinada posição.<br>
	 * <i>Tanto a camada e a posição começam de zero</i>
	 * @param camada - camada onde o neuronio esta
	 * @param posicaoNeuronio - posição do neuronio sobre a camada
	 * @return Neuronio encontrado, ou <code>null</code> caso a camada e a posição sejam invalidas
	 */
	public Neuronio getNeuronio(int camada, int posicaoNeuronio){
		try {
			return neuronios.get(camada)[posicaoNeuronio];
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Metodo para pegar os neuronios que estao na camada anterior e servem como entrada para neurionio da posição descrita
	 * @param camada - camada indicativa onde o neuronio esta, irá retornar a camada anterior. Camada zero sempre sera para camada de entrada.
	 * @param posicaoNeuronio - posicao relativa a onde o neuronio se encontra
	 * @return os neuronios que estao ligado, ou <code>null</code> caso a camada e a posição sejam invalidas
	 */
	public Neuronio[] getNeuroniosEntrada(int camada, int posicaoNeuronio){
		if(camada > 0 && camada < neuronios.size()){
			return neuronios.get(camada-1);
		}
		return null;
	}

	/**
	 * Metodo para pegar os pesos que estão entre o neuronio e a camada anterior
	 * @param camada - camada onde o neuronio esta. Camada um sempre sera para pegar a camada de entrada
	 * @param posicaoNeuronio - posicao relativa a onde o neuronio se encontra
	 * @return os peso que estao ligado, ou <code>null</code> caso a camada e a posição sejam invalidas
	 */
	public Peso[] getPesosEntrada(int camada, int posicaoNeuronio){
		if(camada > 0 && camada < neuronios.size()){
			Peso[][] meusPesos = this.pesos.get(camada-1);
			Peso[] returnPeso = new Peso[meusPesos.length];
			for (int i = 0; i < returnPeso.length; i++) {
				returnPeso[i] = meusPesos[i][posicaoNeuronio];
			}
			return returnPeso;
		}
		return null;
	}
	
	/**
	 * Metodo para pegar os pesos de uma determinada camada.<br>
	 * Pesos começam na camada 0 (pesos entre a camada de entrada e a segunda camada)
	 * @param camada - camada onde estão os pesos
	 * @return pesos da camada, ou <code>null</code> caso a camada seja invalida
	 */
	public Peso[][] getPesos(int camada){
		return pesos.get(camada);
	}

	/**
	 * Atualiza o valor do peso<br>
	 * Metodo chamado por um {@link Atualizar}
	 * @param peso - {@link Peso} na qual tera o valor atulizado
	 * @param valor - valor para atualizacao
	 * @return valor atualizado do peso, ou {@link Integer}.MIN_VALUE caso o {@link Peso} seja <code>null</code>
	 */
	public double atualizarPeso(Peso peso, double valor){
		return getAtualizarPeso().atualizarValor(peso, valor);
	}

	/**
	 * Atualiza o valor do vies<br>
	 * Metodo chamado por um {@link Atualizar}
	 * @param vies - {@link Vies} na qual tera o valor atulizado
	 * @param valor - valor para atualizacao
	 * @return valor atualizado do vies, ou {@link Integer}.MIN_VALUE caso o {@link Vies} seja <code>null</code>
	 */
	public double atualizarVies(Vies vies, double valor){
		return getAtualizarVies().atualizarValor(vies, valor);
	}

	/**
	 * Verifica se esta na hora da rede parar de treinar<br>
	 * Metodo chamado por um {@link TesteParada}
	 * @return true se a rede dever para o treinamento
	 */
	public boolean verificarParada() {
		return getTestePara().virificarPara();
	}

	/**
	 * Calcula a saida de um neuronio que possui uma ligacao entre os neuronios de entrada e os de saida.<br>
	 * Metodo chamado por um {@link CalculoSaida}
	 * @param entadas - neuronios de entrada
	 * @param pesos - pesos ligados ao neuronio
	 * @param responsalvel - neuronio na qual recebe as informações da entrada junto ao seus pesos 
	 * @return valor da saida, ou {@link Integer}.MIN_VALUE caso ocorra algum erro
	 */
	public double calcularSaidaNeuronio(Neuronio []entadas, Peso []pesos, Neuronio responsavel){
		return getCalculoSaida().calcularSaidaNeuronio(entadas, pesos, responsavel);
	}

	/**
	 * Método para verificar as saidas apos o treino
	 * @param entrada - valores de entrada
	 * @return resultado do implementado {@link DefaultVerificarSaida}
	 * @throws RedeNeuralException incompatibilidade entre o tamanho do vetor com os neuronios de entrada
	 */
	public double[] verificarSaidas(double[] entrada) throws RedeNeuralException{
		return getVerificarSaida().verificarSaidas(entrada);
	}
	
	/**
	 * Método para verificar as saidas apos o treino
	 * @param entrada - valores de entrada
	 * @return resultado do implementado {@link DefaultVerificarSaida}
	 * @throws RedeNeuralException incompatibilidade entre o tamanho do vetor com os neuronios de entrada  
	 */
	public List<double[]> verificarSaidas(List<double[]> entrada) throws RedeNeuralException{
		List<double[]> valores = new ArrayList<double[]>();
		for (double[] ds : entrada) {
			valores.add(verificarSaidas(ds));
		}
		return valores;
	}

	/**
	 * Aplica a função de ativação 
	 * @param valor - valor para aplicar a função de ativação 
	 * @return resultado da função
	 */
	public double aplicarFuncaoAtivacao(double valor){
		return getFuncaoAtivacao().funcao(valor);
	}

	/**
	 * Cria os pesos com base nos neuronios
	 */
	private void criarPesos() {
		for (int i = 0; i < neuronios.size()-1; i++) {
			Peso [][]pesos = new Peso [neuronios.get(i).length][neuronios.get(i+1).length];
			for (int j = 0; j < pesos.length; j++) {
				for (int k = 0; k < pesos[j].length; k++) {
					pesos[j][k] = new Peso();
					getInicialiazarValor().inicializarPeso(pesos[j][k]);
				}
			}
			this.pesos.put(i, pesos);
		}

	}

	/**
	 * Cria os neuronios
	 */
	private void criarNeuronios(){
		int camada = 0;
		NeuronioEntrada entradas[] = new NeuronioEntrada[configuracao.getQuantidadeEntrada()];
		for (int i = 0; i < entradas.length; i++) {
			if(configuracao.isSuportaVies()){
				Vies v = new Vies(0);
				getInicialiazarValor().inicializarVies(v);
				entradas[i] = new NeuronioEntrada(0, v);
			}else
				entradas[i] = new NeuronioEntrada(0);
		}
		neuronios.put(camada, entradas);
		
		if(configuracao.getCamadasIntermediarias().size() > 0 && configuracao.isMultiCamadas()){
			for (ConfiguracaoCamadaIntemediaria c : configuracao.getCamadasIntermediarias()) {
				camada++;
				NeuronioIntermediario []intermediario = new NeuronioIntermediario[c.getQtdNeuronios()];
				for (int i = 0; i < intermediario.length; i++) {
					if(configuracao.isSuportaVies()){
						Vies v = new Vies(0);
						getInicialiazarValor().inicializarVies(v);
						intermediario[i] = new NeuronioIntermediario(0, v);
					}else
						intermediario[i] = new NeuronioIntermediario(0);
				} 
				neuronios.put(camada, intermediario);
			}
			
		}
		camada++;
		NeuronioSaida saidas[] = new NeuronioSaida[configuracao.getQuantidadeSaida()];
		for (int i = 0; i < saidas.length; i++) {
			if(configuracao.isSuportaVies()){
				Vies v = new Vies(0);
				getInicialiazarValor().inicializarVies(v);
				saidas[i] = new NeuronioSaida(0, v);
			}else
				saidas[i] = new NeuronioSaida(0);
		}
		neuronios.put(camada, saidas);
	}

	/**
	 * Mostrar algumas configura��es<br>
	 * <b>apenas teste</b>
	 */
	public void mostrarConfiguracoes(){
		System.out.print("Neuronios: ");
		for (Neuronio[] n : neuronios.values()) {
			System.out.print(n.length+ " ");
		}
		System.out.println();
		for (int i = 0; i < neuronios.size(); i++) {
			System.out.println(Arrays.toString(neuronios.get(i)));
		}
		System.out.println();
		
		System.out.println("Pesos: ");
		for (int i = 0; i < pesos.size(); i++) {
			System.out.println("Camada: "+i);
			Peso[][] p = pesos.get(i);
			for (int k = 0; k < p.length; k++) {
				for (int j = 0; j < p[k].length; j++) {
					System.out.print(p[k][j]+" ");
				}
				System.out.println();
			}
		}
		
		System.out.println("Vies:");
		for (int k = 1; k < neuronios.size();k++) {
			Neuronio n[] = neuronios.get(k);
			for (int i = 0; i < n.length; i++) {
				if(n[i].getVies() != null)
					System.out.print(n[i].getVies().getValor()+ " ");
			}
			System.out.println();
		}
	}

	/**
	 * Pegar os neuronios de uma determinada camada
	 * @param camada - camada onde os neuronios se encontram
	 * @return neuronios da camada
	 */
	public Neuronio[] getNeuronios(int camada){
		return neuronios.get(camada);
	}

	/**
	 * Pegar os neuronios da camada de entrada
	 * @return neuronios de entrada
	 */
	public NeuronioEntrada[] getEntradas(){
		return (NeuronioEntrada[]) neuronios.get(0);
	}

	/**
	 * Pegar os neuronios da camada de saida
	 * @return neuronios de saida
	 */
	public NeuronioSaida[] getSaidas(){
		return (NeuronioSaida[]) neuronios.get(neuronios.size()-1);
	}


	/**
	 * @return the atualizarPeso
	 */
	public Atualizar<Peso> getAtualizarPeso() {
		return atualizarPeso;
	}

	/**
	 * @return the atualizarVies
	 */
	public Atualizar<Vies> getAtualizarVies() {
		return atualizarVies;
	}

	/**
	 * @return the calculoSaida
	 */
	public CalculoSaida getCalculoSaida() {
		return calculoSaida;
	}

	/**
	 * @return the testePara
	 */
	public TesteParada getTestePara() {
		return testePara;
	}

	/**
	 * @return the verificarSaida
	 */
	public VerificarSaida getVerificarSaida() {
		return verificarSaida;
	}

	/**
	 * @return the funcaoAtivacao
	 */
	public FuncaoAtivacao getFuncaoAtivacao() {
		return funcaoAtivacao;
	}

	/**
	 * @return the inicialiazarValor
	 */
	public InicialiazarValor getInicialiazarValor() {
		return inicialiazarValor;
	}

	/**
	 * @param atualizarPeso the atualizarPeso to set
	 */
	public void setAtualizarPeso(Atualizar<Peso> atualizarPeso) {
		this.atualizarPeso = atualizarPeso;
	}

	/**
	 * @param atualizarVies the atualizarVies to set
	 */
	public void setAtualizarVies(Atualizar<Vies> atualizarVies) {
		this.atualizarVies = atualizarVies;
	}

	/**
	 * @param calculoSaida the calculoSaida to set
	 */
	public void setCalculoSaida(CalculoSaida calculoSaida) {
		this.calculoSaida = calculoSaida;
	}

	/**
	 * @param testePara the testePara to set
	 */
	public void setTestePara(TesteParada testePara) {
		this.testePara = testePara;
	}

	/**
	 * @param verificarSaida the verificarSaida to set
	 */
	public void setVerificarSaida(VerificarSaida verificarSaida) {
		this.verificarSaida = verificarSaida;
	}

	/**
	 * @param funcaoAtivacao the funcaoAtivacao to set
	 */
	public void setFuncaoAtivacao(FuncaoAtivacao funcaoAtivacao) {
		this.funcaoAtivacao = funcaoAtivacao;
	}

	/**
	 * @param inicialiazarValor the inicialiazarValor to set
	 */
	public void setInicialiazarValor(InicialiazarValor inicialiazarValor) {
		this.inicialiazarValor = inicialiazarValor;
	}

	/**
	 * @return the configuracao
	 */
	public Configuracao getConfiguracao() {
		return configuracao;
	}

}